const { get, each, filter, find } = require('lodash');
const decomment = require('decomment');
const crypto = require('node:crypto');
const fs = require('node:fs');
const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection');
const path = require('node:path');
const { isLargeFile } = require('../../utils/filesystem');

const STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB

const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
  const collectionAuth = get(collectionRoot, 'request.auth');
  if (collectionAuth && request.auth.mode === 'inherit') {
    switch (collectionAuth.mode) {
      case 'awsv4':
        axiosRequest.awsv4config = {
          accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),
          secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),
          sessionToken: get(collectionAuth, 'awsv4.sessionToken'),
          service: get(collectionAuth, 'awsv4.service'),
          region: get(collectionAuth, 'awsv4.region'),
          profileName: get(collectionAuth, 'awsv4.profileName')
        };
        break;
      case 'basic':
        axiosRequest.basicAuth = {
          username: get(collectionAuth, 'basic.username'),
          password: get(collectionAuth, 'basic.password')
        };
        break;
      case 'bearer':
        axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;
        break;
      case 'digest':
        axiosRequest.digestConfig = {
          username: get(collectionAuth, 'digest.username'),
          password: get(collectionAuth, 'digest.password')
        };
        break;
      case 'ntlm':
        axiosRequest.ntlmConfig = {
          username: get(collectionAuth, 'ntlm.username'),
          password: get(collectionAuth, 'ntlm.password'),
          domain: get(collectionAuth, 'ntlm.domain')
        };
        break;
      case 'wsse':
        const username = get(collectionAuth, 'wsse.username', '');
        const password = get(collectionAuth, 'wsse.password', '');

        const ts = new Date().toISOString();
        const nonce = crypto.randomBytes(16).toString('hex');

        // Create the password digest using SHA-1 as required for WSSE
        const hash = crypto.createHash('sha1');
        hash.update(nonce + ts + password);
        const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');

        // Construct the WSSE header
        axiosRequest.headers[
          'X-WSSE'
        ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
        break;
      case 'apikey':
        const apiKeyAuth = get(collectionAuth, 'apikey');
        if (apiKeyAuth.key.length === 0) break;
        if (apiKeyAuth.placement === 'header') {
          axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
        } else if (apiKeyAuth.placement === 'queryparams') {
          // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
          axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
        }
        break;
      case 'oauth2':
        const grantType = get(collectionAuth, 'oauth2.grantType');
        switch (grantType) {
          case 'password':
            axiosRequest.oauth2 = {
              grantType: grantType,
              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
              username: get(collectionAuth, 'oauth2.username'),
              password: get(collectionAuth, 'oauth2.password'),
              clientId: get(collectionAuth, 'oauth2.clientId'),
              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
              scope: get(collectionAuth, 'oauth2.scope'),
              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'authorization_code':
            axiosRequest.oauth2 = {
              grantType: grantType,
              callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),
              authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),
              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
              clientId: get(collectionAuth, 'oauth2.clientId'),
              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
              scope: get(collectionAuth, 'oauth2.scope'),
              state: get(collectionAuth, 'oauth2.state'),
              pkce: get(collectionAuth, 'oauth2.pkce'),
              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'implicit':
            axiosRequest.oauth2 = {
              grantType: grantType,
              callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),
              authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),
              clientId: get(collectionAuth, 'oauth2.clientId'),
              scope: get(collectionAuth, 'oauth2.scope'),
              state: get(collectionAuth, 'oauth2.state'),
              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'client_credentials':
            axiosRequest.oauth2 = {
              grantType: grantType,
              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),
              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),
              clientId: get(collectionAuth, 'oauth2.clientId'),
              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),
              scope: get(collectionAuth, 'oauth2.scope'),
              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),
              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),
              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),
              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),
              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),
              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
        }
        break;
    }
  }

  if (request.auth) {
    switch (request.auth.mode) {
      case 'awsv4':
        axiosRequest.awsv4config = {
          accessKeyId: get(request, 'auth.awsv4.accessKeyId'),
          secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),
          sessionToken: get(request, 'auth.awsv4.sessionToken'),
          service: get(request, 'auth.awsv4.service'),
          region: get(request, 'auth.awsv4.region'),
          profileName: get(request, 'auth.awsv4.profileName')
        };
        break;
      case 'basic':
        axiosRequest.basicAuth = {
          username: get(request, 'auth.basic.username'),
          password: get(request, 'auth.basic.password')
        };
        break;
      case 'bearer':
        axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;
        break;
      case 'digest':
        axiosRequest.digestConfig = {
          username: get(request, 'auth.digest.username'),
          password: get(request, 'auth.digest.password')
        };
        break;
      case 'ntlm':
        axiosRequest.ntlmConfig = {
          username: get(request, 'auth.ntlm.username'),
          password: get(request, 'auth.ntlm.password'),
          domain: get(request, 'auth.ntlm.domain')
        };
      case 'oauth2':
        const grantType = get(request, 'auth.oauth2.grantType');
        switch (grantType) {
          case 'password':
            axiosRequest.oauth2 = {
              grantType: grantType,
              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),
              username: get(request, 'auth.oauth2.username'),
              password: get(request, 'auth.oauth2.password'),
              clientId: get(request, 'auth.oauth2.clientId'),
              clientSecret: get(request, 'auth.oauth2.clientSecret'),
              scope: get(request, 'auth.oauth2.scope'),
              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
              credentialsId: get(request, 'auth.oauth2.credentialsId'),
              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'authorization_code':
            axiosRequest.oauth2 = {
              grantType: grantType,
              callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
              authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),
              clientId: get(request, 'auth.oauth2.clientId'),
              clientSecret: get(request, 'auth.oauth2.clientSecret'),
              scope: get(request, 'auth.oauth2.scope'),
              state: get(request, 'auth.oauth2.state'),
              pkce: get(request, 'auth.oauth2.pkce'),
              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
              credentialsId: get(request, 'auth.oauth2.credentialsId'),
              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'implicit':
            axiosRequest.oauth2 = {
              grantType: grantType,
              callbackUrl: get(request, 'auth.oauth2.callbackUrl'),
              authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),
              clientId: get(request, 'auth.oauth2.clientId'),
              scope: get(request, 'auth.oauth2.scope'),
              state: get(request, 'auth.oauth2.state'),
              credentialsId: get(request, 'auth.oauth2.credentialsId'),
              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
          case 'client_credentials':
            axiosRequest.oauth2 = {
              grantType: grantType,
              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),
              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),
              clientId: get(request, 'auth.oauth2.clientId'),
              clientSecret: get(request, 'auth.oauth2.clientSecret'),
              scope: get(request, 'auth.oauth2.scope'),
              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),
              credentialsId: get(request, 'auth.oauth2.credentialsId'),
              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),
              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),
              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),
              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),
              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),
              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })
            };
            break;
        }
        break;
      case 'wsse':
        const username = get(request, 'auth.wsse.username', '');
        const password = get(request, 'auth.wsse.password', '');

        const ts = new Date().toISOString();
        const nonce = crypto.randomBytes(16).toString('hex');

        // Create the password digest using SHA-1 as required for WSSE
        const hash = crypto.createHash('sha1');
        hash.update(nonce + ts + password);
        const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');

        // Construct the WSSE header
        axiosRequest.headers[
          'X-WSSE'
        ] = `UsernameToken Username="${username}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${ts}"`;
        break;
      case 'apikey':
        const apiKeyAuth = get(request, 'auth.apikey');
        if (apiKeyAuth.key.length === 0) break;
        if (apiKeyAuth.placement === 'header') {
          axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;
        } else if (apiKeyAuth.placement === 'queryparams') {
          // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.
          axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;
        }
        break;
    }
  }

  return axiosRequest;
};

const prepareRequest = async (item, collection = {}, abortController) => {
  const request = item.draft ? item.draft.request : item.request;
  const settings = item.draft?.settings ?? item.settings;
  const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});
  const collectionPath = collection?.pathname;
  const headers = {};
  let contentTypeDefined = false;
  let url = request.url;

  each(get(collectionRoot, 'request.headers', []), (h) => {
    if (h.enabled && h.name?.toLowerCase() === 'content-type') {
      contentTypeDefined = true;
      return false;
    }
  });

  const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';
  const requestTreePath = getTreePathFromCollectionToItem(collection, item);
  if (requestTreePath && requestTreePath.length > 0) {
    mergeHeaders(collection, request, requestTreePath);
    mergeScripts(collection, request, requestTreePath, scriptFlow);
    mergeVars(collection, request, requestTreePath);
    mergeAuth(collection, request, requestTreePath);
    request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
    request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
    request.promptVariables = collection?.promptVariables || {};
  }

  each(get(request, 'headers', []), (h) => {
    if (h.enabled && h.name.length > 0) {
      headers[h.name] = h.value;
      if (h.name.toLowerCase() === 'content-type') {
        contentTypeDefined = true;
      }
    }
  });

  let axiosRequest = {
    mode: request.body.mode,
    method: request.method,
    url,
    headers,
    name: item.name,
    tags: item.tags || [],
    pathParams: request.params?.filter((param) => param.type === 'path'),
    settings,
    responseType: 'arraybuffer'
  };

  axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);

  if (request.body.mode === 'json') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/json';
    }
    try {
      axiosRequest.data = decomment(request?.body?.json);
    } catch (error) {
      axiosRequest.data = request?.body?.json;
    }
  }

  if (request.body.mode === 'text') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'text/plain';
    }
    axiosRequest.data = request.body.text;
  }

  if (request.body.mode === 'xml') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/xml';
    }
    axiosRequest.data = request.body.xml;
  }

  if (request.body.mode === 'sparql') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/sparql-query';
    }
    axiosRequest.data = request.body.sparql;
  }

  if (request.body.mode === 'file') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
    }

    const bodyFile = find(request.body.file, (param) => param.selected);
    if (bodyFile) {
      let { filePath, contentType } = bodyFile;

      axiosRequest.headers['content-type'] = contentType;
      if (filePath) {
        if (!path.isAbsolute(filePath)) {
          filePath = path.join(collectionPath, filePath);
        }

        try {
          // Large files can cause "JavaScript heap out of memory" errors when loaded entirely into memory.
          if (isLargeFile(filePath, STREAMING_FILE_SIZE_THRESHOLD)) {
            // For large files: Use streaming to avoid memory issues
            axiosRequest.data = fs.createReadStream(filePath);
          } else {
            // For smaller files: Use synchronous read for better performance
            axiosRequest.data = fs.readFileSync(filePath);
          }
        } catch (error) {
          console.error('Error reading file:', error);
        }
      }
    }
  }

  if (request.body.mode === 'formUrlEncoded') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
    }
    const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);
    axiosRequest.data = enabledParams;
  }

  if (request.body.mode === 'multipartForm') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'multipart/form-data';
    }
    const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
    axiosRequest.data = enabledParams;
  }

  if (request.body.mode === 'graphql') {
    const graphqlQuery = {
      query: get(request, 'body.graphql.query'),
      // https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation
      variables: decomment(get(request, 'body.graphql.variables') || '{}')
    };
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = 'application/json';
    }
    axiosRequest.data = graphqlQuery;
  }

  // if the mode is 'none' then set the content-type header to false. #1693
  if (request.body.mode === 'none' && request.auth.mode !== 'awsv4') {
    if (!contentTypeDefined) {
      axiosRequest.headers['content-type'] = false;
    }
  }

  if (request.script) {
    axiosRequest.script = request.script;
  }

  if (request.tests) {
    axiosRequest.tests = request.tests;
  }

  axiosRequest.vars = request.vars;
  axiosRequest.collectionVariables = request.collectionVariables;
  axiosRequest.folderVariables = request.folderVariables;
  axiosRequest.requestVariables = request.requestVariables;
  axiosRequest.promptVariables = request.promptVariables;
  axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;
  axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;
  axiosRequest.assertions = request.assertions;
  axiosRequest.oauth2Credentials = request.oauth2Credentials;

  return axiosRequest;
};

module.exports = {
  prepareRequest,
  setAuthHeaders
};
